﻿// --------------------------------
// <copyright file="CharacterPhysics.cs" company="Faculty of Information Technology, Czech Technical University in Prague">
//     This document is shared by Microsoft Public License (MS-PL)
//     For further details please see http://msdn.microsoft.com/en-us/library/ff648068.aspx or license.txt.
// </copyright>
// <author>Vladislav Richter</author>

// <email>richtvl2@fit.cvut.cz</email>

// <date>9. 11. 2011 22:22:37</date>

// <summary>
//
// </summary>
// --------------------------------
using System;
using System.Collections.Generic;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace FITClub.Episode1.Components.Physics
{
    /// <summary>
    /// This class represents physics enveloping each avatar
    /// </summary>
    public class CharacterPhysics
    {
        #region FIELDS

        /// <summary>
        /// Avatr this physics belongs to
        /// </summary>
        public PlayableCharacter Player;
        /// <summary>
        /// Physics body of torso of the avatar
        /// </summary>
        public Body Torso;
        /// <summary>
        /// Fists sensors
        /// </summary>
        public CharacterSensor RFist, LFist;
        /// <summary>
        /// Bounding spheres of the torso
        /// </summary>
        public BoundingSphere[] BoundingSpheres;
        /// <summary>
        /// Motor which allows the avatar to move left and right on the ground
        /// </summary>
        public MotorSphere Motor;
        public Model SphereModel;
        public Matrix DrawOffset;
        public float Scale = 1.0f;
        /// <summary>
        /// Movement constans
        /// </summary>
        public float MotorSpeed = 6.0f, NormalMotorSpeed = 6.0f, SlowMotorSpeed = 3.0f, FastMotorSpeed = 12.0f;
        public float MaximumAirStrafeSpeed = 0.5f;
        public float AirStrafeSpeed = 0.2f;

        #endregion FIELDS

        #region METHODS

        public CharacterPhysics(PlayableCharacter Player)
        {
            this.Player = Player;
            DrawOffset = Matrix.CreateTranslation(0, -0.1f * Scale, 0);
            Torso = new Body(Player.game.PhysicsWorld);
            Torso.BodyType = BodyType.Dynamic;
            Torso.Position = new Vector2(0, 0);
            Torso.FixedRotation = true;
            Torso.UserData = Player;
            //Torso.Enabled = false;
        }

        public void Init()
        {
            BoundingSpheres = new BoundingSphere[2];
            BoundingSpheres[0] = new BoundingSphere(this.Player, new Vector2(0, 0.28f), 0.1f, 1.0f, "head");
            BoundingSpheres[1] = new BoundingSphere(this.Player, new Vector2(0, 0.0f), 0.18f, 1.0f, null);
            //BoundingSpheres[2] = new BoundingSphere(this.Player, new Vector2(0, 0.0f), 0.25f, 1.0f);
            Motor = new MotorSphere(this.Player, new Vector2(0, -0.3f), 0.19f, 1.0f);
            RFist = new CharacterSensor(this, "Hand_R_0");
            LFist = new CharacterSensor(this, "Hand_L_0");
        }

        /// <summary>
        /// Makes avatar's height smaller
        /// </summary>
        public void Duck()
        {
            Motor.Offset = new Vector2(0, -0.11f);
            Motor.Motor.LocalAnchorB = Motor.Offset;
            Motor.World = Matrix.CreateScale(Motor.R * 2.0f) * Matrix.CreateTranslation(Motor.Offset.X, Motor.Offset.Y, 0);
            Motor.MotorWheel.Awake = true;
            Torso.Awake = true;
        }

        /// <summary>
        /// Makes avatar's height normal
        /// </summary>
        public void StandUp()
        {
            Motor.Offset = new Vector2(0, -0.3f);
            Motor.Motor.LocalAnchorB = Motor.Offset;
            Motor.World = Matrix.CreateScale(Motor.R * 2.0f) * Matrix.CreateTranslation(Motor.Offset.X, Motor.Offset.Y, 0);
            Motor.MotorWheel.Awake = true;
            Torso.Awake = true;
        }

        /// <summary>
        /// Makes avatar jump just a little bit - used for fall recovery
        /// </summary>
        public void TinyJump()
        {
            Torso.LinearVelocity = new Vector2(Torso.LinearVelocity.X, 0);
            Motor.MotorWheel.LinearVelocity = new Vector2(Motor.MotorWheel.LinearVelocity.X, 0);
            Torso.ApplyLinearImpulse(new Vector2(0, 0.45f));
        }

        /// <summary>
        /// Makes avatar jump
        /// </summary>
        public void Jump()
        {
            Torso.ApplyLinearImpulse(new Vector2(0, 1.5f));
        }

        /// <summary>
        /// Increases avatar's speed
        /// </summary>
        public void SuperJump()
        {
            Torso.ApplyLinearImpulse(new Vector2(0, 2.5f));
        }

        #region MOVE CONTROL

        public void MoveLeft()
        {
            if (Motor.Contacts.Count > 0)
            {
                Player.Direction = +1;
                MotorMoveLeft();
            }
            else AirMovementLeft();
        }

        public void AirMovementLeft()
        {
            MotorStop();
            Player.Direction = +1;
            if (Motor.Contacts.Count == 0 && Torso.LinearVelocity.X > -MaximumAirStrafeSpeed)
            {
                Torso.ApplyLinearImpulse(new Vector2(-AirStrafeSpeed, 0));
            }
        }

        public void MoveRight()
        {
            if (Motor.Contacts.Count > 0)
            {
                Player.Direction = -1;
                MotorMoveRight();
            }
            else AirMovementRight();
        }

        public void AirMovementRight()
        {
            MotorStop();
            Player.Direction = -1;
            if (Motor.Contacts.Count == 0 && Torso.LinearVelocity.X < +MaximumAirStrafeSpeed)
            {
                Torso.ApplyLinearImpulse(new Vector2(+AirStrafeSpeed, 0));
            }
        }

        #endregion MOVE CONTROL

        #region MOTOR CONTROL

        public void SetNormalSpeed()
        {
            MotorSpeed = NormalMotorSpeed;
        }

        public void SetSlowSpeed()
        {
            MotorSpeed = SlowMotorSpeed;
        }

        public void SetFastSpeed()
        {
            MotorSpeed = FastMotorSpeed;
        }

        public void MotorMoveLeft()
        {
            Motor.Motor.MotorSpeed = -MotorSpeed;
        }

        public void MotorMoveRight()
        {
            Motor.Motor.MotorSpeed = MotorSpeed;
        }

        public void MotorStop()
        {
            Motor.Motor.MotorSpeed = 0;
        }

        #endregion MOTOR CONTROL

        public void Update()
        {
            RFist.Update();
            LFist.Update();
        }

        /// <summary>
        /// Moves avatar to different position
        /// </summary>
        /// <param name="Position">Coordinates</param>
        /// <param name="Angle">Angle of avatar's rotation</param>
        public void SetPosition(Vector2 Position, float Angle)
        {
            Torso.Rotation = 0;
            Torso.FixedRotation = true;
            Torso.ResetDynamics();            
            Torso.SetTransformIgnoreContacts(ref Position, Angle);
            Torso.Awake = true;
            if (Motor != null)
            {
                Motor.MotorWheel.ResetDynamics();
                Motor.MotorWheel.SetTransformIgnoreContacts(ref Position, Angle);
                Motor.MotorWheel.Awake = true;
            }
        }

        public void SetPosition(ref Vector2 position)
        {
            //Action<Vector2> v2 = SetPositionX;
            Torso.SetTransformIgnoreContacts(ref position, Torso.Rotation);
            Motor.MotorWheel.SetTransformIgnoreContacts(ref position, Torso.Rotation);
            //is.Player.Position = position;
        }

        /// <summary>
        /// Gets avatar's center position
        /// </summary>
        /// <returns></returns>
        public Vector2 GetPosition()
        {
            return Torso.Position;
        }

        /// <summary>
        /// Draws bounding boxes - osed for debug only!
        /// </summary>
        public void Draw()
        {
            Player.game.GraphicsDevice.BlendState = BlendState.Additive;

            for (int i = 0; i < BoundingSpheres.Length; i++) BoundingSpheres[i].Draw();
            Motor.Draw();

            Player.game.GraphicsDevice.BlendState = BlendState.Opaque;
            Player.game.GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            RFist.Draw();
            LFist.Draw();
        }

        #endregion METHODS
    }

    /// <summary>
    /// This class represents single bounding sphere enveloping avatar
    /// </summary>
    public class BoundingSphere
    {
        #region FIELDS

        public float R;
        public Vector2 Offset;
        public Matrix World;
        public PlayableCharacter Character;
        public Fixture Fix;

        #endregion FIELDS

        #region METHODS

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="Player">Avatar this spehere belongs to</param>
        /// <param name="Offset">Offset to avatars center</param>
        /// <param name="R">Sphere radius</param>
        /// <param name="Density">Mass density</param>
        /// <param name="data">Identification data</param>
        public BoundingSphere(PlayableCharacter Player, Vector2 Offset, float R, float Density, Object data)
        {
            this.Character = Player;
            this.Offset = Offset * Player.CharPhysx.Scale;
            this.R = R * Player.CharPhysx.Scale;
            this.Fix = FixtureFactory.AttachCircle(R, Density, Player.CharPhysx.Torso, this.Offset, data);
            this.Fix.Friction = 0.1f;
            this.Fix.OnCollision += BoundingSphereOnCollision;
            this.Fix.OnSeparation += BoundingSphereOnSeparation;
            this.World = Matrix.CreateScale(this.R * 2.0f) * Matrix.CreateTranslation(Offset.X, Offset.Y, 0);
        }

        #region COLLISION DETECTION

        public bool BoundingSphereOnCollision(Fixture f1, Fixture f2, Contact contact)
        {
            //if (f2.Body != Player.CharPhysx.Torso && f2.Body != Player.CharPhysx.Motor.MotorWheel && f2.Body.UserData != null) { if ((f2.Body.UserData as string) != "fist" && (f2.Body.UserData as string) != "powerup") Player.contacts.Add(contact); }
            //else Player.contacts.Add(contact);
            if (f2.UserData != null)
            {
                //Console.WriteLine(Player.Name + " contact with something not null");
                if ((f2.UserData as string) == "head") { Character.contacts.Add(contact); }// Console.WriteLine(Player.Name + " contact with head"); }
            }
            else Character.contacts.Add(contact);
            //Console.WriteLine(Player.Name + " contacts.count = " + Player.contacts.Count);
            return true;
        }

        public void BoundingSphereOnSeparation(Fixture f1, Fixture f2)
        {
            foreach (Contact c in Character.contacts)
            {
                if ((c.FixtureA == f1 && c.FixtureB == f2) || (c.FixtureA == f2 && c.FixtureB == f1))
                {
                    Character.contacts.Remove(c);
                    //Console.WriteLine(Player.Name + " contacts.count = " + Player.contacts.Count);
                    break;
                }
            }
        }

        #endregion COLLISION DETECTION

        /// <summary>
        /// Debug only!!!
        /// </summary>
        public void Draw()
        {
            Matrix WorldT = World * Matrix.CreateRotationZ(Character.CharPhysx.Torso.Rotation) * Matrix.CreateTranslation(new Vector3(Character.Position, 0));
            foreach (ModelMesh mesh in Character.CharPhysx.SphereModel.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    //effect.Alpha = 1.0f;
                    //effect.TextureEnabled = false;
                    effect.DiffuseColor = new Vector3(1.0f, 0.0f, 1.0f);
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.World = WorldT;
                    effect.View = Character.game.ViewMatrix;
                    effect.Projection = Character.game.ProjectionMatrix;
                }
                mesh.Draw();
            }
        }

        #endregion METHODS
    }

    /// <summary>
    /// This class represents feet of the avatar - special bounding box, which also allows movement
    /// </summary>
    public class MotorSphere
    {
        #region FIELDS

        public float R;
        public Vector2 Offset;
        public Matrix World;
        public PlayableCharacter Character;
        public Fixture Fix;
        public Body MotorWheel;
        public FarseerPhysics.Dynamics.Joints.RevoluteJoint Motor;
        public List<Contact> Contacts = new List<Contact>();

        #endregion FIELDS

        #region METHODS

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="Character">Avatar this spehere belongs to</param>
        /// <param name="Offset">Offset to avatars center</param>
        /// <param name="R">Sphere radius</param>
        /// <param name="Density">Mass density</param>
        public MotorSphere(PlayableCharacter Character, Vector2 Offset, float R, float Density)
        {
            this.Character = Character;
            this.Offset = Offset * Character.CharPhysx.Scale;
            this.R = R * Character.CharPhysx.Scale;
            MotorWheel = new Body(Character.game.PhysicsWorld);
            MotorWheel.BodyType = BodyType.Dynamic;
            MotorWheel.Position = Character.CharPhysx.GetPosition() + this.Offset;
            MotorWheel.UserData = Character;
            //MotorWheel.Enabled = false;
            this.Fix = FixtureFactory.AttachCircle(R, Density, MotorWheel, Vector2.Zero);
            this.Fix.Friction = 0.9f;
            this.Fix.OnCollision += MotorSphereOnCollision;
            this.Fix.OnSeparation += MotorSphereOnSeparation;
            Motor = new RevoluteJoint(MotorWheel, Character.CharPhysx.Torso, Vector2.Zero, this.Offset);
            Motor.CollideConnected = false;
            Motor.MotorEnabled = true;
            Motor.MaxMotorTorque = 1000f;
            Motor.MotorSpeed = 0f;
            this.Character.game.PhysicsWorld.AddJoint(Motor);
            this.World = Matrix.CreateScale(this.R * 2.0f) * Matrix.CreateTranslation(Offset.X, Offset.Y, 0);
        }

        #region COLLISION DETECTION

        public bool MotorSphereOnCollision(Fixture f1, Fixture f2, Contact contact)
        {
            if (f2.UserData != null)
            {
                //Console.WriteLine(Player.Name + " contact with something not null");
                if ((f2.UserData as string) == "head")
                {
                    Character.contacts.Add(contact);
                    this.Contacts.Add(contact);
                }//Console.WriteLine(Player.Name + " contact with head"); }
            }
            else if (f2.Body.UserData != null)
            {
                if ((f2.Body.UserData as string) != "respawn")
                {
                    Character.contacts.Add(contact);
                    this.Contacts.Add(contact);
                }
            }
            else
            {
                Character.contacts.Add(contact);
                //if (f2.Body != Player.CharPhysx.Torso && f2.Body != Player.CharPhysx.Motor.MotorWheel && f2.Body.UserData != null) { if ((f2.Body.UserData as string) != "fist" && (f2.Body.UserData as string) != "powerup") Player.contacts.Add(contact); }
                //else Player.contacts.Add(contact);
                Contacts.Add(contact);
            }
            //Console.WriteLine(Player.Name+" contacts.count = " + Player.contacts.Count);
            return true;
        }

        public void MotorSphereOnSeparation(Fixture f1, Fixture f2)
        {
            foreach (Contact c in Character.contacts)
            {
                if ((c.FixtureA == f1 && c.FixtureB == f2) || (c.FixtureA == f2 && c.FixtureB == f1))
                {
                    Character.contacts.Remove(c);
                    //Console.WriteLine(Player.Name + " contacts.count = " + Player.contacts.Count);
                    break;
                }
            }

            foreach (Contact c in Contacts)
            {
                if ((c.FixtureA == f1 && c.FixtureB == f2) || (c.FixtureA == f2 && c.FixtureB == f1))
                {
                    Contacts.Remove(c);
                    break;
                }
            }
        }

        #endregion COLLISION DETECTION

        /// <summary>
        /// Debug only!
        /// </summary>
        public void Draw()
        {
            Matrix WorldT = Matrix.CreateRotationZ(MotorWheel.Rotation) * World * Matrix.CreateTranslation(new Vector3(Character.Position, 0));
            foreach (ModelMesh mesh in Character.CharPhysx.SphereModel.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    //effect.Alpha = 1.0f;
                    //effect.TextureEnabled = false;
                    effect.DiffuseColor = new Vector3(1.0f, 1.0f, 0.0f);
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.World = WorldT;
                    effect.View = Character.game.ViewMatrix;
                    effect.Projection = Character.game.ProjectionMatrix;
                }
                mesh.Draw();
            }
        }

        #endregion METHODS
    }
}